package gov.nasa.jpl.hi.marsimages.rovers;
import android.util.Log;
import com.evernote.edam.type.Note;
import com.evernote.edam.type.Resource;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import gov.nasa.jpl.hi.marsimages.models.CameraModel;
import gov.nasa.jpl.hi.marsimages.models.M;
import static gov.nasa.jpl.hi.marsimages.MarsImagesApp.TAG;
/**
* Created by mpowell on 4/12/14.
*/
public abstract class Rover {
public static final String CURIOSITY = "Curiosity";
public static final String OPPORTUNITY = "Opportunity";
public static final String SPIRIT = "Spirit";
public static final String TAG = "Rover";
static final String SOL = "Sol";
static final String LTST = "LTST";
static final String RMC = "RMC";
private static final double EARTH_SECS_PER_MARS_SEC = 1.027491252;
private static final DateFormat DATE_FORMAT = SimpleDateFormat.getDateInstance();
static final String EPOCH_FORMAT = "yyyyMMddhh:mm:sszzz";
int instrumentIndex;
int eyeIndex;
int sampleTypeIndex;
Map<Integer, List<CSVRecord>> locationsBySite = new HashMap<Integer, List<CSVRecord>>();
final Set<String> stereoInstruments = new HashSet<>();
public abstract String getUser();
public abstract String getSortableImageFilename(String sourceURL);
public abstract String getLabelText(Note note);
public String getSectionText(Note note) {
int sol = getSol(note);
double interval = sol * 24 * 60 * 60 * EARTH_SECS_PER_MARS_SEC;
GregorianCalendar calendar = new GregorianCalendar();
Date epoch = getEpoch();
calendar.setTime(epoch);
calendar.add(Calendar.SECOND, (int) interval);
String formattedDate = DATE_FORMAT.format(calendar.getTime());
return "Sol " + sol + " " + formattedDate;
}
public abstract int getSol(Note note);
protected abstract Date getEpoch();
public abstract String getURLPrefix();
public abstract String getDetailText(Note note);
public abstract String getCaptionText(Note note);
public abstract String getImageName(Resource resource);
public abstract double getCameraFOV(String cameraId);
public abstract String getCameraId(String imageID);
public abstract float getMastX();
public abstract float getMastY();
public abstract float getMastZ();
public String getImageID(Resource resource) {
String url = resource.getAttributes().getSourceURL();
String[] tokens = url.split("[\\./]");
int numTokens = tokens.length;
return tokens[numTokens - 2];
}
public abstract String[] stereoForImages(Note note);
public JSONArray modelJson(Note note) {
try {
String cameraModelJson = note.getResources().get(0).getAttributes().getCameraModel();
if (cameraModelJson != null && cameraModelJson.length() > 0) {
try {
return new JSONArray(cameraModelJson);
} catch (JSONException e) {
Log.e(TAG, "Bad json in image note " + note.getTitle() + " " + e.getMessage());
}
}
}catch (NullPointerException e) {
Log.e(TAG, "Null stuff "+ note + ", "+ note.getResources().size());
}
return null;
}
public double fieldOfView(Note image) {
String imageID = getImageID(image.getResources().get(0));
String cameraId = getCameraId(imageID);
return getCameraFOV(cameraId);
}
public static boolean includedInMosaic(Note note) {
String title = note.getTitle();
return title.contains("Navcam") ||
title.contains("Mastcam Left") ||
title.contains("Pancam");
}
public List<CSVRecord> siteLocationData(int siteIndex) {
//return cached results if we have them
//TODO invalidate cache when needed (after a day?)
List<CSVRecord> cachedLocations = locationsBySite.get(siteIndex);
if (cachedLocations != null) return cachedLocations;
List<CSVRecord> locations = new ArrayList<>();
URL locationsURL = null;
try {
String sixDigitSite = String.format("%06d", siteIndex);
locationsURL = new URL(getURLPrefix() + "/locations/site_"+sixDigitSite+".csv");
Log.d(TAG, "location url: %@" + locationsURL);
HttpURLConnection urlConnection = (HttpURLConnection)locationsURL.openConnection();
try {
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
final Reader reader = new InputStreamReader(in);
final CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT);
try {
for (final CSVRecord record : parser) {
locations.add(record);
}
locationsBySite.put(siteIndex, locations);
} finally {
parser.close();
reader.close();
}
} finally {
urlConnection.disconnect();
}
} catch (MalformedURLException e) {
Log.e(TAG, "Badly formatted URL for location manifest: " + locationsURL);
} catch (IOException e) {
Log.e(TAG, "Error reading from location manifest URL: " + locationsURL);
}
return locations;
}
public double[] localLevelQuaternion(int siteIndex, int driveIndex) {
List<CSVRecord> locationData = siteLocationData(siteIndex);
double[] q = new double[4];
for (CSVRecord location : locationData) {
if (location.size() >= 5 && driveIndex == Integer.parseInt(location.get(0))) {
q[0] = Double.parseDouble(location.get(1));
q[1] = Double.parseDouble(location.get(2));
q[2] = Double.parseDouble(location.get(3));
q[3] = Double.parseDouble(location.get(4));
break;
}
}
return q;
}
public abstract int getLayer(String cameraId, String imageId);
/**
* Created by mpowell on 5/3/14.
*/
public abstract static class MER extends Rover {
public static final String COURSE = "Course";
enum TitleState {
START,
SOL_NUMBER,
IMAGESET_ID,
INSTRUMENT_NAME,
MARS_LOCAL_TIME,
DISTANCE,
YAW,
PITCH,
ROLL,
TILT,
ROVER_MOTION_COUNTER
}
@Override
public String getCameraId(String imageId) {
if (imageId.contains("Sol")) { //Color Pancam image IDs begin with "Sol"...
return "P";
}
return imageId.substring(1,2);
}
public int getLayer(String cameraId, String imageId) {
if (cameraId.charAt(0) == 'N') {
return 3;
} else if (!imageId.contains("Sol")) {
return 2;
}
return 1;
}
@Override
public double getCameraFOV(String cameraId) {
char camera = cameraId.charAt(0);
if (camera == 'N')
return 0.78539816;
else if (camera == 'P')
return 0.27925268;
Log.e(TAG, "Unexpected camera id for FOV check: "+cameraId);
return 0;
}
public float getMastX() {
return 0.456f;
}
public float getMastY() {
return 0.026f;
}
public float getMastZ() {
return -1.0969f;
}
@Override
public String getSortableImageFilename(String sourceURL) {
String[] tokens = sourceURL.split("/");
String filename = tokens[tokens.length - 1];
if (filename.startsWith("Sol"))
return "0"; //sort Cornell Pancam images first
else if ((filename.startsWith("1") || filename.startsWith("2")) && filename.length() == 31)
return filename.substring(23);
return filename;
}
@Override
public String getCaptionText(Note note) {
if (note == null) return "";
MERTitle title = tokenize(note.getTitle());
if (title.distance == 0.0f)
return String.format("%s image taken on Sol %d.", title.instrumentName, title.sol);
else
return String.format("Drive for %.2f meters on Sol %d.", title.distance, title.sol);
}
@Override
public String getLabelText(Note note) {
if (note == null) return "";
MERTitle merTitle = tokenize(note.getTitle());
if (merTitle.distance == 0f)
return merTitle.instrumentName;
else
return String.format("Drive for %.2f meters", merTitle.distance);
}
@Override
public String getDetailText(Note note) {
if (note == null) return "";
String marstime = tokenize(note.getTitle()).marsLocalTime;
return (marstime != null) ? marstime + " LST" : "";
}
@Override
public int getSol(Note note) {
if (note == null) return 0;
String title = note.getTitle();
String tokens[] = title.split(" ");
if (tokens.length >= 2) {
return Integer.parseInt(tokens[1]);
}
return 0;
}
public MERTitle tokenize(String title) {
MERTitle mer = new MERTitle();
String[] tokens = title.split(" ");
TitleState state = TitleState.START;
for (String word : tokens) {
switch (word) {
case SOL:
state = TitleState.SOL_NUMBER;
continue;
case LTST:
state = TitleState.MARS_LOCAL_TIME;
continue;
case RMC:
state = TitleState.ROVER_MOTION_COUNTER;
continue;
}
String[] indices;
switch (state) {
case START:
break;
case SOL_NUMBER:
mer.sol = Integer.parseInt(word);
state = TitleState.IMAGESET_ID;
break;
case IMAGESET_ID:
if (word.equals(COURSE)) {
mer = parseCoursePlotTitle(title, mer);
return mer;
} else {
mer.imageSetID = word;
}
state = TitleState.INSTRUMENT_NAME;
break;
case INSTRUMENT_NAME:
if (mer.instrumentName == null) {
mer.instrumentName = word;
} else {
mer.instrumentName += " " + word;
}
break;
case MARS_LOCAL_TIME:
mer.marsLocalTime = word;
break;
case ROVER_MOTION_COUNTER:
indices = word.split("-");
mer.siteIndex = Integer.parseInt(indices[0]);
mer.driveIndex = Integer.parseInt(indices[1]);
break;
default:
Log.w("mer title", "Unexpected state in parsing image title: " + state);
break;
}
}
return mer;
}
public MERTitle parseCoursePlotTitle(String title, MERTitle mer) {
String[] tokens = title.split(" ");
TitleState state = TitleState.START;
for (String word : tokens) {
switch (word) {
case COURSE:
mer.instrumentName = "Course Plot";
break;
case "Distance":
state = TitleState.DISTANCE;
continue;
case "yaw":
state = TitleState.YAW;
continue;
case "pitch":
state = TitleState.PITCH;
continue;
case "roll":
state = TitleState.ROLL;
continue;
case "tilt":
state = TitleState.TILT;
continue;
case "RMC":
state = TitleState.ROVER_MOTION_COUNTER;
continue;
}
String[] indices;
switch (state) {
case START:
break;
case DISTANCE:
mer.distance = Float.parseFloat(word);
break;
case YAW:
mer.yaw = Float.parseFloat(word);
break;
case PITCH:
mer.pitch = Float.parseFloat(word);
break;
case ROLL:
mer.roll = Float.parseFloat(word);
break;
case TILT:
mer.tilt = Float.parseFloat(word);
break;
case ROVER_MOTION_COUNTER:
indices = word.split("-");
mer.siteIndex = Integer.parseInt(indices[0]);
mer.driveIndex = Integer.parseInt(indices[1]);
break;
default:
Log.w("mer title", "Unexpected state in parsing course plot title: " + state);
break;
}
}
return mer;
}
@Override
public String getImageName(Resource resource) {
String imageid = getImageID(resource);
if (resource.getAttributes().getSourceURL().contains("False"))
return "Color";
String instrument = imageid.substring(instrumentIndex, instrumentIndex + 1);
if (instrument.equals("N") || instrument.equals("F") || instrument.equals("R")) {
String eye = imageid.substring(eyeIndex, eyeIndex + 1);
if (eye.equals("L"))
return "Left";
else
return "Right";
} else if (instrument.equals("P")) {
return imageid.substring(eyeIndex, eyeIndex + 2);
}
return "";
}
public String[] stereoForImages(Note note) {
if (note == null || note.getResources().size() == 0)
return new String[0];
String imageid = getImageID(note.getResources().get(0));
String instrument = imageid.substring(instrumentIndex, instrumentIndex + 1);
if (!stereoInstruments.contains(instrument) && !imageid.startsWith("Sol"))
return new String[0];
int leftImageIndex = -1;
int rightImageIndex = -1;
int index = 0;
for (Resource resource : note.getResources()) {
imageid = getImageID(resource);
String eye = imageid.substring(eyeIndex, eyeIndex + 1);
if (leftImageIndex == -1 && eye.equals("L") && !imageid.startsWith("Sol"))
leftImageIndex = index;
if (rightImageIndex == -1 && eye.equals("R"))
rightImageIndex = index;
index += 1;
}
if (leftImageIndex >= 0 && rightImageIndex >= 0) {
return new String[]{
note.getResources().get(leftImageIndex).getAttributes().getSourceURL(),
note.getResources().get(rightImageIndex).getAttributes().getSourceURL()};
}
return new String[0];
}
}
}